Skip to content

Conversation

@alizaberger
Copy link
Collaborator

@alizaberger alizaberger commented Mar 4, 2025

Describe your changes

Fixes the issue described in this meta post. As the user types, preview container content is removed before the promise is resolved, causing the page layout to shift during the time it takes for the promise to resolve. The fix is to not clear the preview, rather wait for the content to be replaced when the promise is resolved.

Before

screen-capture.1.webm

After

screen-capture.webm

PR Checklist

  • All new/changed functionality includes unit and (optionally) e2e tests as appropriate
  • All new/changed functions have /** ... */ docs
  • I've added the bug/enhancement and other labels as appropriate

Environment(s) tested

  • Device: [e.g. desktop, iPhone6, etc.]
  • OS: [e.g. iOS]
  • Browser [e.g. chrome, safari]
  • Version [e.g. 22]

Additional context

Add any other context about the PR here.

@alizaberger alizaberger requested review from alexwarren, b-kelly and threefjefff and removed request for b-kelly March 4, 2025 03:10
@netlify
Copy link

netlify bot commented Mar 4, 2025

Deploy Preview for stacks-editor ready!

Name Link
🔨 Latest commit ea45a8c
🔍 Latest deploy log https://app.netlify.com/sites/stacks-editor/deploys/67cb143da6bc110008bfe91b
😎 Deploy Preview https://deploy-preview-389--stacks-editor.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@alexwarren
Copy link
Collaborator

One regression I notice with this change is that we can end up with multiple spinners created by examplePreviewRenderer:

Screen.Recording.2025-03-04.at.10.14.35.mov

Maybe that was why there was this code for clearing out the preview in the first place?

I'm not sure if that means there is something to be fixed in examplePreviewRenderer itself, or if there needs to be some way of cancelling a call to the preview.renderer if there is one in-flight when a newer edit is made.

@alizaberger alizaberger changed the title Don't clear the preview before rerendering fix(preview): Fix constant layout shift as preview rerenders Mar 4, 2025
@giamir
Copy link
Contributor

giamir commented Mar 4, 2025

I'm not sure if that means there is something to be fixed in examplePreviewRenderer itself, or if there needs to be some way of cancelling a call to the preview.renderer if there is one in-flight when a newer edit is made.

Renderers are async functions: in the current implementation once we call them there is no way to cancel the side effects that are tasked to do from the editor codebase unfortunately. What we can do though is to pass in an AbortController signal as option that the consumers implementing the renderer can leverage to abort whatever they are doing.

This is how the update preview method will change (notice the addition of a new abortController class member)

private updatePreview(text: string) {
    this.container.innerHTML = "";

    // if showing the preview, fire off the renderer async
    if (this.isShown) {
        
       // PART OF CODE REMOVED BY THIS PR
           
        this.container.appendChild(this.dom);

        // Abort any previous render
        if (this.abortController) {
            this.abortController.abort();
        }

        // Create a new AbortController for the current render
        this.abortController = new AbortController();
        const signal = this.abortController.signal;

        void this.renderer?.(text, this.dom, { signal }).catch((e) => {
            if (e.name !== 'AbortError') {
                error(
                    "PreviewView.updatePreview",
                    `Uncaught exception in preview renderer`,
                    e
                );
            }
        });
    }
}

Then on the consumer end you can consume the signal. In the case of our examplePreviewRenderer in the site/index.ts file the part of sleepAsync(500) would become:

const interruptableSleepAsync = (delayMs, signal) => new Promise((resolve, reject) => {
        const timeoutId = setTimeout(() => {
            resolve();
        },  delayMs);

        signal.addEventListener("abort", () => {
            clearTimeout(timeoutId);
             reject(new Error('aborted'));
         });
 });
 
 interruptableSleepAsync(500, signal)
 .then(() => {
   // do what we were doing before
 })
 .catch(() => {
   // cleanup any lingering dom elements added previously if necessary (e.g. remove the spinner)
 })

Now you need to consider that in Core the renderer is different and a bit more sophisticated / legacy.
https://github.com/StackEng/StackOverflow/blob/main/StackOverflow/_Scripts/PartialJS/stacks-editor.mod.ts#L514-L520
Code will need to be adjusted in there so that the renderer can accept the signal and use it to prevent async side effects from happening when an in-flight render is aborted.

I am tagging @b-kelly since I don't know why we don't have already a mechanism to instruct consumers preview renderers to interrupt in-flight rendering. Perhaps we were relying on the fact that race conditions for async renders fired one after the other would be unlikely.

@alizaberger
Copy link
Collaborator Author

@giamir That looks like a nice pattern to interrupt the renderer, I will work on implementing that

@alizaberger
Copy link
Collaborator Author

Here's what the preview pane looks like with the AbortController at work @giamir @alexwarren

screen-capture.2.webm

@giamir
Copy link
Contributor

giamir commented Mar 7, 2025

@alizaberger before investing time on implementing the mechanism to interrupt in-flight rendering it would be interesting to know if that would be useful in our Core context in my opinion.

You could go in Core to StackOverflow/node_modules/@stackoverflow/stacks-editor/dist/commonmark/plugins/preview.js and remove line 71 and 72 as you have done in this PR and then run npm start and check the specific page where the bug was reproduced.

@giamir
Copy link
Contributor

giamir commented Mar 7, 2025

Here's what the preview pane looks like with the AbortController at work @giamir @alexwarren

I think that's what I would expect based on how the examplePreviewRenderer is implemented in the demo site. We should not based our decision though on what that renderer but rather on what the renderer of Core does. This is why I was suggesting to check how your small changes behave in the Core context. If it does its job and doesn't have strange spinner appearing I would argue to keep the initial changes you made and adjust the implementation of the examplePreviewRenderer of the demo site so that we don't get that spinner cascade (perhaps we could remove the spinner all together from the renderer implementation). Hope my suggestion makes sense. If not feel free to DM me. :)

@alizaberger
Copy link
Collaborator Author

Final screen capture:

screen-capture.3.webm

@alizaberger alizaberger requested a review from dancormier March 7, 2025 15:45
Copy link
Contributor

@dancormier dancormier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @alizaberger! I tested this change both in the standalone editor and by duplicating this change the editor in Core. Everything worked as I expected 🎉

20250307-125818

My only (optional) suggestion is adding a test for this functionality. I'm fine with seeing this PR merged in as-is, but I think it could be worth trying to add tests to any code that's touched that doesn't already include a test case. Since this change is so minor, I'm ok with it not getting a test at the moment, but if you have a cycle or two to try an add one, I think it would be handy to have added.

@alizaberger alizaberger merged commit be56d39 into main Mar 7, 2025
5 checks passed
@alizaberger alizaberger deleted the aberger/preview-render-layout-shifts branch March 7, 2025 18:55
This was referenced Apr 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants